package com.zzw.paymentdemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.zzw.paymentdemo.config.WxPayConfig;
import com.zzw.paymentdemo.entity.OrderInfo;
import com.zzw.paymentdemo.entity.RefundInfo;
import com.zzw.paymentdemo.enums.OrderStatus;
import com.zzw.paymentdemo.enums.wxpay.WxApiType;
import com.zzw.paymentdemo.enums.wxpay.WxNotifyType;
import com.zzw.paymentdemo.enums.wxpay.WxTradeState;
import com.zzw.paymentdemo.service.OrderInfoService;
import com.zzw.paymentdemo.service.PaymentInfoService;
import com.zzw.paymentdemo.service.RefundInfoService;
import com.zzw.paymentdemo.service.WxPayService;
import com.zzw.paymentdemo.util.NoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author zzw
 * @date 2023/5/11 16:35
 */
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Autowired
    private WxPayConfig wxPayConfig;

    @Autowired
    private OrderInfoService orderInfoService;

    @Autowired
    private CloseableHttpClient wxHttpClient;

    @Autowired
    private PaymentInfoService paymentInfoService;

    @Autowired
    private RefundInfoService refundInfoService;

//定义 ReentrantLock 进行并发控制。注意，必须手动释放锁。
    private final ReentrantLock lock = new ReentrantLock();

    /**
     *创建订单，调用Native支付接口
     * @author zzw
     * @date 2023/5/11 17:28
     * @param productId
     * @return code_url ， orderNo
     */
    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {
        log.info("生成订单");

//        生成订单
        OrderInfo orderInfo=orderInfoService.createOrderByProductId(productId);

        String codeUrl = orderInfo.getCodeUrl();
//        二维码有效期为2h，这里只是单纯判断后保存，没有再进行期限效验
        if(orderInfo!=null&& !StringUtils.isEmpty(codeUrl)){
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        }


        log.info("调用统一下单API");
//        调用统一下单API
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));


        // 请求body参数
        Gson gson=new Gson();
        Map paramsMap=new HashMap();
        paramsMap.put("appid",wxPayConfig.getAppid());
        paramsMap.put("mchid",wxPayConfig.getMchId());
        paramsMap.put("description",orderInfo.getTitle());
        paramsMap.put("out_trade_no",orderInfo.getOrderNo());
        paramsMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
        Map amountMap=new HashMap();
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");
        paramsMap.put("amount",amountMap);

//        将参数转换成json字符串
        String paramsJson = gson.toJson(paramsMap);
        log.info("请求参数：{}",paramsJson);
        StringEntity entity = new StringEntity(paramsJson,"utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);
        try{

            String bodyAsString = EntityUtils.toString(response.getEntity());  //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应码
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回参数体 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                System.out.println("success");
            } else {
                log.error("失败, 响应码 = " + statusCode+ ",返回体 = " + bodyAsString);
                throw new IOException("request failed");
            }
//            响应结果
            HashMap resultMap = gson.fromJson(bodyAsString, HashMap.class);

//            二维码
             codeUrl = (String)resultMap.get("code_url");

//            保存二维码
            QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("order_no",orderInfo.getOrderNo());
            orderInfo.setCodeUrl(codeUrl);
            orderInfoService.update(orderInfo,queryWrapper);

//             返回二维码
            Map<String,Object>map=new HashMap<>();
            map.put("codeUrl",codeUrl);
            map.put("orderNo",orderInfo.getOrderNo());
            return map;
        } finally {
            response.close();
        }
    }
    /**
     *回调通知后处理订单
     * @param bodyMap
     **/
    @Override
    public void processOrder(Map<String,Object> bodyMap) throws GeneralSecurityException {

        log.info("处理订单");
//        对称解密
        String plainText=this.decryptFromResource(bodyMap);
//        转换明文
        Gson gson=new Gson();
        Map<String,Object> plainTextMap = gson.fromJson(plainText, Map.class);
//        获取订单号
        String orderNo = (String) plainTextMap.get("out_trade_no");


        /*在对业务数据进行状态检查和处理之前，
            要采用数据锁进行并发控制，
            以避免函数重入造成的数据混乱*/
        //尝试获取锁：
        // 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if(lock.tryLock()){
            try {
//        处理重复通知
//        保证接口调用的幂等性，无论接口调用多少次，产生的结果是一致的
                String orderStatus=orderInfoService.getOrderStatus(orderNo);
                if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){
                    return;
                }


//                //模拟通知并发
//                try {
//                    TimeUnit.SECONDS.sleep(5);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }



//        更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
//        记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            }finally {
                //要主动释放锁
                lock.unlock();
            }
        }
}

    /**
     *用户取消订单
     * @param orderNo
     **/
    @Override
    public void cancelOrder(String orderNo) throws IOException {
//        调用微信支付支付的关单接口
        this.closeOrder(orderNo);
//        更新商户端的订单状态
        orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
    }


    /**
     * 根据订单号查询微信支付查单接口，核实订单状态
     * 如果订单已支付，则更新商户端订单状态，并记录支付日志
     * 如果订单未支付，则调用关单接口关闭订单，并更新商户端订单状态
     * @param orderNo
     */

    @Override
    public void checkOrderStatus(String orderNo) throws Exception {

        log.warn("根据订单号核实订单状态 ===> {}", orderNo);

        //调用微信支付查单接口
        String result = this.queryOrder(orderNo);
        Gson gson=new Gson();
        Map resultMap = gson.fromJson(result, HashMap.class);

        //获取微信支付端的订单状态
        Object tradeState = resultMap.get("trade_state");
        //判断订单状态
        if(WxTradeState.SUCCESS.getType().equals(tradeState)){
            log.warn("核实订单已支付 ===> {}", orderNo);
            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(result);
        }
        if(WxTradeState.NOTPAY.getType().equals(tradeState)){
            log.warn("核实订单未支付 ===> {}", orderNo);
            //如果订单未支付，则调用关单接口
            this.closeOrder(orderNo);

            //更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }

    }

    /**
     * 退款
     * @author zzw
     * @date 2023/5/16 18:23
     * @param orderNo
     * @param reason
     */
    @Transactional(rollbackFor = Exception.class)  //发生Exception错误时事务回滚
    @Override
    public void refunds(String orderNo, String reason) throws Exception {
        log.info("创建退款单记录");
//        根据订单号和原因创建退款单
        RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
        log.info("调用退款API");
//         调用退款API
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost=new HttpPost(url);

        // 请求body参数
        Gson gson = new Gson();
        Map paramsMap = new HashMap();
        paramsMap.put("out_trade_no", orderNo);//订单编号
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());//退款单编号
        paramsMap.put("reason",reason);//退款原因
        paramsMap.put("notify_url",
                wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//退款通知地址

        Map amountMap = new HashMap();
        amountMap.put("refund", refundInfo.getRefund());//退款金额
        amountMap.put("total", refundInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");//退款币种
        paramsMap.put("amount", amountMap);


//        将参数转换成json字符串
        String paramsJson = gson.toJson(paramsMap);
        log.info("请求参数：{}",paramsJson);
        StringEntity entity = new StringEntity(paramsJson,"utf-8");
        entity.setContentType("application/json"); //设置请求报文格式
        httpPost.setEntity(entity); //将请求报文放入请求对象
        httpPost.setHeader("Accept", "application/json"); //设置响应报文格式

        //完成签名并执行请求,并完成验签
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);
        try{
            //解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());  //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应码
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回参数体 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                System.out.println("success");
            } else {
                log.error("失败, 响应码 = " + statusCode+ ",返回体 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //更新订单状态
            orderInfoService.updateStatusByOrderNo(orderNo,
                    OrderStatus.REFUND_PROCESSING);

//            更新退款单
            refundInfoService.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

    /**
     *处理退款单
     * @param bodyMap
     **/
    @Transactional(rollbackFor = Exception.class)  //发生Exception错误时事务回滚

    @Override
    public void processRefund(Map<String, Object> bodyMap) throws Exception {
        log.info("处理退款单");
//        对称解密
        String plainText=this.decryptFromResource(bodyMap);
//        转换明文
        Gson gson=new Gson();
        Map<String,Object> plainTextMap = gson.fromJson(plainText, Map.class);
//        获取订单号
        String orderNo = (String) plainTextMap.get("out_trade_no");


        /*在对业务数据进行状态检查和处理之前，
            要采用数据锁进行并发控制，
            以避免函数重入造成的数据混乱*/
        //尝试获取锁：
        // 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if(lock.tryLock()){
            try {
//        处理重复通知
//        保证接口调用的幂等性，无论接口调用多少次，产生的结果是一致的
                String orderStatus=orderInfoService.getOrderStatus(orderNo);
                if(!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)){
                    return;
                }


//                //模拟通知并发
//                try {
//                    TimeUnit.SECONDS.sleep(5);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }



//        更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
//        记录支付日志
                refundInfoService.updateRefund(plainText);
            }finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }


    /**
     *关单接口的调用
     * @param orderNo
     **/
    private void closeOrder(String orderNo) throws IOException {
        log.info("关单接口的调用，订单号 ===> {}", orderNo);
//      创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);

//      组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap<>();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        String jsonParams = gson.toJson(paramsMap);
        log.info("请求参数 ===> {}", jsonParams);

//        将请求参数设置到请求对象中
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpPost);

        try {
            int statusCode = response.getStatusLine().getStatusCode(); //响应码
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功204");
            } else {
                log.error("失败, 响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        }finally {
            response.close();
        }
    }


    /**
     *对称解密
     * @param bodyMap
     * @return java.lang.String
     **/
    private String decryptFromResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密");
//        通知数据
        Map<String,String> resourceMap = (Map) bodyMap.get("resource");
//      数据密文
        String ciphertext = resourceMap.get("ciphertext");
//      随机串
        String nonce = resourceMap.get("nonce");
//      附加数据
        String associatedData = resourceMap.get("associated_data");
        log.info("密文 ===> {}", ciphertext);

        AesUtil aesUtil=new AesUtil(wxPayConfig.getApiV3Key().getBytes());
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                    nonce.getBytes(StandardCharsets.UTF_8), ciphertext);
        log.info("明文 ===> {}", plainText);
        return plainText;

    }

    /**
     * 微信查单接口调用
     */
    public String queryOrder(String orderNo) throws IOException {
        log.info("查单接口的调用，订单号 ===> {}", orderNo);

//      创建远程请求对象
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);

        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxHttpClient.execute(httpGet);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());  //响应体
            int statusCode = response.getStatusLine().getStatusCode(); //响应码
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功204");
            } else {
                log.error("失败, 响应码 = " + statusCode);
                throw new IOException("request failed");
            }
            return bodyAsString;
        }finally {
            response.close();
        }

    }

}
